- web6047 - (2021/09/10(金) 現在、システム調整中のため、一部の表示がおかしいかもしれません)

homepage6047 2019年 7月

プログラミングやRPG(作るほう)が好きな人の日記

2019/7/31(水)

iTunes で購入した曲

人が購入した曲が何なのか聞いて、訪問者の方が楽しいかどうかわかりませんが…

今回 7/12~7/31 のあいだに、7 曲購入しました。


(訪問者のどんなニーズと この記事がつながるか)


最近 ページの扉絵、扉スクリプトがないのは

パソコンの使用を制限しているからです。

パソコンをやりすぎて不健康になってしまうので、平日は基本的に「WEB観賞」と「iTunes で何を観た、買ったといった一般的な記事の作成」だけにして、プログラミングは土曜の午後に限るようにしています。

そのためなかなか時間を取れないでいます。


2019/7/27(土)

RPG の試作2 -[rpg]

fig.
▲RPG「マラカト・アルバトリ」

RPG のイベントスクリプトまわりの試作品 掲載2回目です。

先日の 7/21(日)に公開したものに、戦闘画面を加えました。敵キャラはスライムと、オークの2匹が出現します。

体力とか攻撃力などの情報はまだないので、「たたかう」を選ぶとモンスターが少し しゃべる…というそれだけの戦闘(?)です。

左の画像リンクをクリックすると JavaScript を実行します。

操作方法:

キー: 移動

EnterまたはSPACEキー: メッセージを送ったり、「はい/いいえ」の選択を決定。



戦闘システムをどうプログラムするか

ドラクエみたいなターン制の戦闘システムは、メインプログラムと同じプログラム言語で作るか、それともゲームイベントスクリプト(「キャラが何歩 歩いたらメッセージを表示する」とか、RPG の脚本にあたるデータで、簡易的なプログラム言語で記述される)で作るか、どちらにすべきか私は未経験のため知らなかったんですが、今回はゲームイベントスクリプトとして戦闘システムを作りました。

…と言っても、プログラム言語もゲームイベントスクリプトも、ここでは同じ JavaScript なので、大きな違いはないかもしれませんが…。


敵キャライラスト

掲載すると にぎやかになりそうなので、敵キャラ2体を掲載します。

fig.
▲スライム

スライム。

プレイヤーがスライムに対して「かいしんのいちげき」を出すと、「はまち!!」と言います。

アクションRPGの「ハイドライド」で有名なメッセージです。

左の画像リンクをクリックすると 画像を拡大 します。



fig.
▲オーク

オーク。

オークって豚の怪物で良いんでしたっけ?

そのへん確認もしないでブタで描いちゃいました。

左の画像リンクをクリックすると 画像を拡大 します。


ドラクエは敵キャラが正面を向いていて「不敵にこちらを にらんでいるのがイイ!」と言われているので、正面向きというのは良いのかもしれません。


戦闘システムを付けたら魅力が上がった

先日 7/21(日)の試作では戦闘がなくて さびしい感じがしていましたが、今回 戦闘システムを付け加えたら急に魅力的に見えてきました。

ゲームではなく、イラストでも描いている途中のものに早いうちから、髪の毛を付けてやったり、目に輝きを付けてやったり、肌を肌色にしたりとすることで、急に魅力的に見えて創作にエンジンがかかるのと同じかなと思いました。


余談ですが、RPG ってもともとは西洋の戦争をボードゲームで再現したものが起源なので、戦いで魅力を放つゲームなのかなっていうのはあります。


お手元で実行できるファイル一式ダウンロード

プログラムはあまりきれいじゃありませんが…ダウンロードだけできるようにしておきます。

公開日 ファイル名 サイズ
2019/7/27 20190727-RPG test.zip 12KB

RPG試作プログラム(JavaScript)の一式をまとめたZIPファイルです。
戦闘画面を用意。
ダウンロード


(訪問者のどんなニーズと この記事がつながるか)


2019/7/26(金)

3DCG 落ちてくるカット -[modeling]

仕事の休憩時間に RPG のシーンを考えました。

―――空を見上げると、何かが飛んでいる…。それは急降下でこちらへ向かってきた!


Shade3D でそのカメラワークの試作を作ってみました。


▼MP4形式/長さ13秒/サイズ2.57MB

東京都のビルの窓みたいな物体ですね。

Shade3D ではカメラの設定で、「ある形状の位置とカメラの向きを連動させる」というボタン「連動」があります。(下図)

連動設定したいカメラをこの画面で選び、連動したい目標の形状を選択してから「連動」ボタンを押します。

この設定のおかげでこの動画は比較的簡単に作成できました。


(訪問者のどんなニーズと この記事がつながるか)


2019/7/21(日)

RPG の試作 -[rpg]

fig.
▲RPG「マラカト・アルバトリ」

RPG のイベントスクリプトまわりの試作品です。

左の画像リンクをクリックすると JavaScript を実行します。

―――アラビア半島を舞台にした物語

プレイヤーは勇者となり、悪政をしいる国王「シャフリヤール」を説得する旅に出ます――

試作品のため、画面のマス目にはマップ画像ではなく座標が振られており、プレイヤーは赤い「●」です。戦闘画面はなく、コマンド画面もありません。

また、情報を提供してくれる村人のような存在もありませんので、点在するイベント(赤いマス)の意味も分からないかもしれません。

キー: 移動

EnterまたはSPACEキー: メッセージを送ったり、「はい/いいえ」の選択を決定。



Web検索で「JavaScript RPG」を検索すると、完成されたようなものがあまり出てこなくて、私が作るこういった試作品も完成度が低く、訪問者の皆さんにとっては「こんなもんか…」と残念に思うんじゃないかと思います。

しかし私にとっては、長年(30年)の RPG 開発についての目的が、大きく前進した試作です。

長年なかなか進まなかった理由が、単に「JavaScript が delay 関数に対応していないから」、

ということが初めて分かりました。

delay 関数は、プログラムを一定時間停止する関数で、RPG のイベントスクリプト実行時は、文字を1文字ずつ表示したり、イベントを少し止めて効果的に演出したりと、わりと必要な関数です。

RPG 開発をしている他の皆さんも delay 関数がないから JavaScript で RPG を作れないでいるんじゃないでしょうか。

delay 関数を async, await, Promise で実現することで、スラスラとプログラムが前進しました。

そんなあっけないものだったのか、と少し拍子抜けしています。

もうしばらくして完成度の高いものが訪問者の皆さんに提供出来たらいいなと思っています。


メインプログラム

こちらはシステム部分です。

← このアイコンをクリックするとプログラムを表示します。
<!DOCTYPE html><!--ESCAPEPROCESS-->

<head>

<title>UntitledHomepage6047Document</title>

<meta content="text/html; charset=UTF-8" http-equiv="content-type">

<script>console.clear();</script>

<script>

/*

19/07/20(土) 22:39:09


RPG試作


ブラウザ対応確認:

Windows10 Firefox ver. 68.0.1

Windows10 Chrome ver. 75.0

Windows10 Edge ver 44.18362.1.0


非対応:

InternetExplorer ver. 11.0.135(IEは async, await, Promise など非対応)



このスクリプトは特に著作権その他の権利を主張しませんので、気にせず自由に使ってください。


スクリプトの作者:homepage6047 d_kawakawaより


*/


function onloadx() {

console.log( "onload()" );


//キー入力

if( 1 ) {

keys = new Array();

keysOne = new Array(); //setIntervalよりも短い瞬間にkeydown,keyupされたときでも、1回実行を行わせる。

onkeydown = function( e ) {

if( keys.indexOf( e.which ) == -1 ) {

keys.push( e.which );

keysOne.push( e.which );

}

}

onkeyup = function( e ) {

var idx = keys.indexOf( e.which );

if( idx != -1 ) keys.splice( idx, 1 );

}

}


/*

このプログラムのポイント:


●時間がなかったので、戦闘画面とコマンド画面は作っていません。

イベント実行の試作として作りました。


●タイトル画面、マップ画面など、各画面は Scene クラスです。

キー入力、描画などを それぞれの画面で個別に用意しています。


●通常、RPG のイベントは独自のスクリプト言語を用意しますが、ここでは JavaScript で記述しています。


以前、delay 関数を実現するために簡易的に JavaScript のエンジンを作ったことがありますが、

今回は async, await, Promise などで delay 関数を実現しています。


delay 関数はメッセージを話しているかのように1文字ずつ表示したり、

話の展開を少し間を開けて効果的にしたりといった目的で使用します。

delay 関数の代わりに setTimeout を用いるのは難しいです。

たとえば、RPG のイベントスクリプトの途中で setTimeout を行い、その途中に再び戻ってくるというのは

JavaScript のエンジンを自分で作るか、簡易的なスクリプトエンジンを自分で作るか、のいずれかになります。

「命令リストの順次実行と IF 文による分岐とジャンプだけあれば、RPG のイベントとしては足りる」という意見が多いと思いますが、JavaScript をそのまま実行できるというメリットは自由度の面で大きいと思います。

実際 screenplay.js を書いているときはその自由度を大きく感じました。

RPG のイベント部分を、別ファイル screenplay.js へ分けてあります。



●Promise を使って一定時間停止関数(delay)や、キー入力待ち関数(hitanykey)を実現しています。

どの関数を async と宣言し、どの関数を await にするか等、Promise は理解がちょっと難しいです。


うまく説明できるかわかりませんが説明してみます。

初心者向けの説明なので、Promise の説明として正確ではないところもあるかもしれません。



たとえば、下記のようなプログラムを書きたいとして、


func1()

ここで一時停止したい

func3()


もし、delay関数があれば以下ように記述できます。


func1()

delay( 100 )

func2()


しかし、JavaScript には delay 関数は用意されていません。

そこで、JavaScript の Promise を使って delay 関数を作成します。


function delay( ms ) {

return new Promise( function( promiseOK ) { setTimeout( promiseOK, ms ) } );

}


この関数で難しいのは、


1. new Promise() のカッコの中に関数が書いてある書き方が難しく感じる。

2. どうして new Promise() の引数に関数を渡すのか、その関数がどう使われるのか、わからない。

3. その関数が promiseOK という変数を引数にとっていて、setTimeout でそれを関数として呼び出していること。

4. Promise を return して、その後何が起こるのかよくわからない。

5. Promise自体の promise というネーミングがわからない。



1. new Promise() のカッコの中に関数が書いてある書き方が難しく感じる。

について、


わかりやすい書き方として、


function delay( ms ) {

var f = function( promiseOK ) {

setTimeout( promiseOK, ms );

}

return new Promise( f );

}


こういう書き方と同じです。

JavaScript は関数を変数に代入できます。

new Promise() は引数として関数を受け取ります。



2. どうして new Promise() のカッコの中に関数を書くのか、その関数がどう使われるのか、わからない。

3. その関数が promiseOK という変数を引数にとっていて、setTimeout でそれを関数として呼び出していること。

の2つについて、


new Promise() のカッコの中に書かれた関数は、とりあえず普通に実行されます。


なお、たとえばキー入力処理を書くとき


window.onkeydown = function( e ) { alert( e.which ); }


のように書いたりしますが、このときの e が JavaScript のシステムが渡してくれるのと同じように、

new Promise() のカッコの中の関数が受け取る引数 promiseOK も JavaScript のシステムが渡してくれるものです。


new Promise() のカッコの中に書かれた関数の中で、都合のいいタイミングで、

処理が終わったことを promiseOK 関数を使うことで JavaScript のシステムに教えてあげます。

都合のいいタイミングとは、例のように数秒後に教えてあげれば、delay関数の実現になります。


return new Promise( function( promiseOK ) { setTimeout( promiseOK, ms ) } );


また、キー入力後とするなら、


return new Promise( function( promiseOK ) { onkeydown = function( e ) { promiseOK(); } } );


と書きます。

この2つの例はそれぞれ、たとえば delay(), hitanykey() という関数に書きますが、

それぞれ呼び出すときは、


await delay();


や、


await hitanykey();


のように await を付けて呼び出します。

プログラムは、await のついた関数の呼び出しで一時停止し、new Promise() による promiseOK() が実行されたところで、

一時停止を解除します。



4. Promise を return して、その後何が起こるのかよくわからない。


1 async function test() {

2 alert( 1 );

3 await delay( 1000 );

4 alert( 2 );

5 }

6 function delay( ms ) {

7 return new Promise( function( promiseOK ) { setTimeout( promiseOK, ms ); } );

8 }


上記3行目で await 付きで delay() などが呼ばれて、7行目で delay() は new Promise() を return しています。

await と return new Promise() は1組で対応しています。


3行目の await でプログラムは一時停止し、

7行目の return new Promise() で new Promise() のカッコの中に書かれた関数が実行され、

その関数の中に書かれた promiseOK 関数が実行されることで、一時停止が解除されます。


繰り返しになりますが、

promiseOK() は自由なタイミングで呼び出すように new Promise() に渡す関数の中で書けばいいので、

setTimeout で一定時間後にしたり、

onkeydown でキー入力後にしたりと、できるわけです。


なお、await を記述するとき、その1つ上の関数は1行目のように async で宣言しなければなりません。



5. Promise自体の promise というネーミングがわからない。


よく知りませんが、

処理を二手に分けて、「こういう処理をしたのちまた1つに戻ろう」という約束、ってことでしょうか。


*/



//各DIV要素

playerDiv = document.getElementById( "player" ) ;

messageDiv = document.getElementById( "message" );

yesnoDiv = document.getElementById( "yesno" );



//各画面について

titleScene = new Scene( "title", document.getElementById( "title" ) );

lastScene = new Scene( "last", document.getElementById( "last" ) );

mapScene = new Scene( "map", document.getElementById( "map" ) );


//マップ上でのキー入力

mapScene.keyexec = function( key ) {

var maxX = eval( currentMap.id + "_cols" );

var maxY = eval( currentMap.id + "_rows" );

switch( key ) {

case 37: if( sp.player.x > 0 ) { sp.player.x --; sp.playerXYChanged = true; } break;

case 38: if( sp.player.y > 0 ) { sp.player.y --; sp.playerXYChanged = true; } break;

case 39: if( sp.player.x < maxX - 1 ) { sp.player.x ++; sp.playerXYChanged = true; } break;

case 40: if( sp.player.y < maxY - 1 ) { sp.player.y ++; sp.playerXYChanged = true; } break;

}


}

//マップ上でのフレームごとの処理

mapScene.frame = function() {

sp.playerXYChanged = false;

for( var i = 0; i < keys.length; i++ ) {

var key = keys[ i ];

this.keyexec( key );

//check. 1回実行したから keysOne から削除

var idx = keysOne.indexOf( key );

if( idx != -1 ) keysOne.splice( idx, 1 );

}

//setIntervalよりも短い瞬間に keydown, keyup されたときでも、1回実行を行わせる。

for( var i = 0; i < keysOne.length; i++ ) this.keyexec( keysOne[ i ] );

keysOne = []; //すべて1回実行したから全消去


this.draw();

//check. キャラ移動した場合、イベント発生をチェック

if( sp.playerXYChanged ) sp.check();

}

//マップ描画

mapScene.draw = function() {

var div = document.getElementById( "board" ).getBoundingClientRect();

var table = document.getElementById( currentMap.id + "_table" ).getBoundingClientRect();

var td = document.getElementById( currentMap.id + " " + sp.player.x + "," + sp.player.y ).getBoundingClientRect();

//プレイヤーキャラを描画

playerDiv.style.left = td.left - div.left + "px";

playerDiv.style.top = td.top - div.top + "px";

}



//各マップを連想配列に集合

maps = new Object();

array = document.getElementsByClassName( "maps" );

for( var i = 0; i < array.length; i++ ) {

var div = array[ i ];

maps[ div.id ] = div;

}

currentMap = maps.radatomcastle;



//イベントのある場所を赤く示す

for( var name in sp.xy ) {

var td = document.getElementById( name );

td.style.backgroundColor = "salmon";

console.log( td.style.backgroundColor );

}



//開始イベント実行

sp.poweron();


}//onloadx()



//---


//画面クラス (タイトル画面、マップ画面など)

function Scene( id, div ) {

this.id = id;

this.div = div;

}

Scene.prototype.open = function() {

currentScene = this;

this.div.style.display = "block";

timerID = setInterval( this.frame.bind( this ), 200 );

}

Scene.prototype.close = function() {

this.div.style.display = "none";

clearInterval( timerID );

}

Scene.prototype.frame = function() {

//必要に応じてインスタンスのほうに定義すること







}

Scene.prototype.draw = function() {

//必要に応じてインスタンスのほうに定義すること







}

Scene.prototype.keyexec = function( key ) {

//必要に応じてインスタンスのほうに定義すること

}



//---


//マップ変更

function goto( name, x, y ) {

//現在のマップを消す

playerDiv.style.visibility = "hidden";

currentMap.style.display = "none";


//新しいマップを表示

currentMap = maps[ name ];

sp.player.x = x;

sp.player.y = y;

currentMap.style.display = "block";

playerDiv.style.display = "block";

currentScene.draw(); //その座標で描画

playerDiv.style.visibility = "visible";

}


//メッセージ表示

async function message( msg, continual ) {

//メッセージ画面を表示

messageDiv.style.display = "block";


//1文字ずつ表示

for( var i = 0; i < msg.length; i++ ) {

messageDiv.innerHTML += msg.charAt( i );

await delay( 100 );

}


//キー入力待ちだと教えるしるし

messageDiv.innerHTML += '<span id="triangle">▼</span>';

var span = document.getElementById( "triangle" );

triangleAnimateID = setInterval( function() {

span.style.visibility = span.style.visibility == "hidden" ? "visible" : "hidden";

}, 500 );


//キー入力待ち

await hitanykey();


//キー入力待ちだと教えるしるしを削除

clearInterval( triangleAnimateID );

messageDiv.removeChild( span );


//check. メッセージ画面を消さない指定がある

if( continual ) {

messageDiv.innerHTML += "<BR><BR>";

return;

}


//メッセージ画面を消す

messageDiv.style.display = "none";

messageDiv.innerHTML = "";

}


//はい、いいえ待ち

async function yesno( msg ) {

//メッセージ表示

messageDiv.style.display = "block";

for( var i = 0; i < msg.length; i++ ) {

messageDiv.innerHTML += msg.charAt( i );

await delay( 100 );

}


//はい、いいえ選択

yesnoDiv.style.display = "block";

await menuselect();


//メッセージ消去

yesnoDiv.style.display = "none";

messageDiv.style.display = "none";

messageDiv.innerHTML = "";


return menuselected == 1;

}


//メニュー選択 手続き

function menuselect() {

return new Promise( function( promiseOK ) {

//初期状態

document.getElementById( "menu1" ).style.backgroundColor = "lightgreen";

menuselected = 1;


//キー入力ハンドラをバックアップ

backup1 = onkeydown;

backup2 = onkeyup;


//キー入力設定

onkeydown = null;

onkeyup = function( e ) {

switch( e.which ) {

case 38://↑

document.getElementById( "menu1" ).style.backgroundColor = "lightgreen";

document.getElementById( "menu2" ).style.backgroundColor = "transparent";

menuselected = 1;

break;

case 40://↓

document.getElementById( "menu1" ).style.backgroundColor = "transparent";

document.getElementById( "menu2" ).style.backgroundColor = "lightgreen";

menuselected = 2;

break;

case 32://space

case 13://enter

//キー入力ハンドラを元に戻す

onkeydown = backup1;

onkeyup = backup2;

promiseOK();

break;

}

}

} );

}


//キー入力待ち

function hitanykey() {

return new Promise( function( promiseOK ) {

//キー入力ハンドラをバックアップ

backup1 = onkeydown;

backup2 = onkeyup;


//キー入力設定

onkeydown = null;

onkeyup = function( e ) {

//キー入力ハンドラを元に戻す

onkeydown = backup1;

onkeyup = backup2;

promiseOK();

}

} );

}


//指定時間待つ

function delay( ms ) {

return new Promise( function( promiseOK ) { setTimeout( promiseOK, ms ) } );

}


//何も受け付けなくする(ゲームオーバー時)

function freez() {

clearInterval( timerID );

}


</script>

<style>

.testTD {

border : solid 1px black;

width : 32px;

height : 32px;

font-size : 10px;

}

.screens {

display : none;

}

.maps {

display : none;

}

.menuitem {

}

#map {

width : 512px;

height : 480px;

background-color : lightgray;

color : black;

}

#arefgald td {

background-color : lightgreen;

}

#radatomcastle td {

background-color : darkred;

color : white;

}

html {

width : 100%;

height : 100%;

display : table;

}

body {

display : table-cell;

text-align : center;

vertical-align : middle;

}

.test {

position : relative;

width : 512px;

height : 480px;

margin : auto;

text-align : left;

}

</style>

</head>

<body onload="if( typeof preload !== 'undefined' ) preload(); else onloadx();">

<div class="test" id="board">

<div class="screens" id="title">

<div style="

background-color : lightblue;

display : table;

width : 512px;

height : 480px;

">

<div style="display:table-cell;

display : table-cell;

text-align : center;

vertical-align : middle;

">

<div style="">

マラカト・アルバトリ(勇者の戦い)<BR>

<BR>

<span style="color:darkred;">HIT ANY KEY</span>

</div>

</div>

</div>

</div>


<div class="screens" id="last" style="display:zblock;">

<div style="

background-color : black;

color : white;

display : table;

width : 512px;

height : 480px;

">

<div style="display:table-cell;

display : table-cell;

text-align : center;

vertical-align : middle;

">

<div style="">

マラカト・アルバトリ(勇者の戦い) - Fin<BR>

<BR>

製作:web6047

</div>

</div>

</div>

</div>


<div class="screens" id="map">


<div class="maps" id="arefgald">

アラビア半島

<table id="arefgald_table">

<script>

var mapname = "arefgald";

arefgald_rows = 10;

arefgald_cols = 10;

document.open();

for( var r = 0; r < arefgald_rows; r++ ) {

document.write( '<tr>' );

for( var c = 0; c < arefgald_cols; c++ ) {

var id = mapname + ' ' + c + ',' + r;

document.write( '<td class="testTD" id="' + id + '">' );

document.write( c + ',' + r );

document.write( '</td>' );

}

document.write( '</tr>' );

}

document.close();

</script>

</table>

</div>


<div class="maps" id="radatomcastle">

大臣の ごうかな おやしき

<table id="radatomcastle_table">

<script>

var mapname = "radatomcastle";

radatomcastle_rows = 5;

radatomcastle_cols = 5;

document.open();

for( var r = 0; r < radatomcastle_rows; r++ ) {

document.write( '<tr>' );

for( var c = 0; c < radatomcastle_cols; c++ ) {

var id = mapname + ' ' + c + ',' + r;

document.write( '<td class="testTD" id="' + id + '">' );

document.write( c + ',' + r );

document.write( '</td>' );

}

document.write( '</tr>' );

}

document.close();

</script>

</table>

</div>


<div id="player" style="

display : none;

position : absolute;

font-size : 32px;

color : red;

"></div>


<div id="message" style="

display : none;

position : absolute;

font-size : 16px;

width : 320px;

height : 160px;

border : solid 3px black;

background-color : white;

color : black;

left : calc( ( 512px - 320px ) / 2 - 1em );

bottom : 24px;

padding : .25em .5em;

">


</div>


<div id="yesno" style="

display : none;

position : absolute;

font-size : 16px;

border : solid 3px black;

background-color : white;

color : black;

padding : .25em .5em;

right : calc( ( 512px - 320px ) / 2 - 2em );

bottom : 12px;

">

<div class="menuitem" id="menu1">はい</div>

<div class="menuitem" id="menu2">いいえ</div>

</div>


</div>

</div>



</body>

<script src="screenplay.js"></script>

</html>



イベントスクリプト

こちらはデータです。RPG の物語を記述しています。

← このアイコンをクリックするとプログラムを表示します。
//SCREENPLAY(シナリオ) jsファイル


screenplay = sp = new Object();


sp.player = new Object();

sp.player.x = 0;

sp.player.y = 0;

sp.player.items = new Array();



//イベントチェック

sp.check = async function() {

var playerXY = currentMap.id + " " + this.player.x + "," + this.player.y;

for( var name in this.xy ) {

if( name == playerXY ) {

//イベント中はマップ移動処理しない

var backup1 = onkeydown;

var backup2 = onkeyup;

onkeydown = null;

onkeyup = null;

keys = new Array();


//イベント実行

await delay( 100 ); //コマ跳び防止用

await this.xy[ name ].bind( this )();


//マップ移動処理再開

onkeydown = backup1;

onkeyup = backup2;

break;

}

}


};


//---各イベント


//プログラム開始時

sp.poweron = async function() {

//タイトル画面

titleScene.open();

await hitanykey();


//オープニング

titleScene.close();

mapScene.open();

goto( "radatomcastle", 2, 2 );

await message( "大臣は言いました。", true );

await message( "勇者よ!よくぞ まいられた!" );

await message( "悪政を しいる「シャフリヤール王」を説得し、この国に平和をもたらしてくれ!" );

await message( "ではゆけ!勇者よ!" );

}


//マップ上の各座標に乗ったとき

sp.xy = {


"radatomcastle 2,4" : async function() {

//宮殿から出る


goto( "arefgald", 5, 4 );

},


"arefgald 5,4" : async function() {

//宮殿へ入る


goto( "radatomcastle", 2,4 );

},


"radatomcastle 4,4" : async function() {

//check. 扉をあける呪文が必要

if( this.player.items.indexOf( "扉をあける呪文" ) == -1 ) {

await message( "扉をあける呪文がないので、未知のエリアに入れない!" );

return;

}

if( this.player.items.indexOf( "魔法のランプ" ) == -1 ) {

await message( "魔法のランプを手に入れた!" );

this.player.items.push( "魔法のランプ" );

}

},



"arefgald 5,1" : async function() {


if( this.player.items.indexOf( "魔法の金貨" ) == -1 ) {

await message( "魔法の金貨を持ってきたら勇者と認めよう!" );


} else {

if( this.player.items.indexOf( "丈夫なじゅうたん" ) == -1 ) {

await message( "魔法の金貨を持ってきたのか!" );

await message( "ではお前を勇者と認め、丈夫なじゅうたんを授けよう!", true );

await message( "丈夫なじゅうたんを手に入れた!" );

this.player.items.push( "丈夫なじゅうたん" );

}

}

},


"arefgald 1,1" : async function() {

//check. 扉をあける呪文が必要

if( this.player.items.indexOf( "扉をあける呪文" ) == -1 ) {

await message( "扉をあける呪文がないので、未知のエリアに入れない!" );

return;

}

if( this.player.items.indexOf( "魔法の金貨" ) == -1 ) {

await message( "魔法の金貨を手に入れた!" );

this.player.items.push( "魔法の金貨" );

}

},


"arefgald 4,9" : async function() {

if( this.player.items.indexOf( "高価なお酒" ) == -1 ) {

await message( "高価なお酒を手に入れた!" );

this.player.items.push( "高価なお酒" );

}

},


"arefgald 7,6" : async function() {

if( this.player.items.indexOf( "扉をあける呪文" ) == -1 ) {

await message( "扉をあける呪文を手に入れた!" );

this.player.items.push( "扉をあける呪文" );

}

},


"arefgald 9,9" : async function() {


if( this.player.items.indexOf( "魔法のランプ" ) == -1 ) {

await message( "立派な祭壇がある!" );

} else {

await message( "立派な祭壇の前で魔法のランプをこすると魔神が現れた!" );

//check. 酒を持っているか

if( this.player.items.indexOf( "高価なお酒" ) == -1 ) {

await message( "高価なお酒を持ってきたら願いをかなえてあげよう!" );

return;

}


await message( "高価なお酒を飲みながら魔神は言いました。", true );

await message( "なんでも願いをかなえてあげよう!" );

if( this.player.items.indexOf( "丈夫なじゅうたん" ) == -1 ) {

await message( "願いはないのか?", true );

await message( "いつでも呼んでくれれば願いをかなえてあげるよ。" );

} else {

await message( "「丈夫なじゅうたんを、空飛ぶじゅうたんに変えてほしい」だって?" );

await message( "よしきた、かなえてあげよう!", true );

await message( "アーブラ・カタブラー!", true );

await message( "丈夫なじゅうたんは、空飛ぶじゅうたんになった!" );

await message( "魔神は言いました。", true );

await message( "それではまた会おう!" );

var idx = this.player.items.indexOf( "丈夫なじゅうたん" );

this.player.items.splice( idx, 1 );

this.player.items.push( "空飛ぶじゅうたん" );

}

}

},


"arefgald 4,6" : async function() {


if( this.player.items.indexOf( "空飛ぶじゅうたん" ) == -1 ) {

await message( "海の向こうにシャフリヤール王の宮殿が見える!" );

} else {

await message( "空飛ぶじゅうたんに乗って、海を越えた!" );

await message( "シャフリヤール王の宮殿に来た!" );

await message( "シャフリヤール王は言いました、", true );

var res = await yesno( "わしのけらいになったら、全財産の半分をやろう。どうじゃ?" );

if( res ) {

await message( "あなたはシャフリヤール王のけらいになった!", true );

await message( "ゲームオーバー", true );

messageDiv.style.backgroundColor = "red";

freez();

} else {

await message( "あなたは、シャフリヤール王に面白い話をいっぱい聞かせました!" );

await message( "機嫌を良くした王は、大臣の願いを聞き入れることにし、人々が喜ぶ国にすると約束しました!" );

mapScene.close();

lastScene.open();

}

}

},


}




公開日 ファイル名 サイズ
2019/7/21 20190721-RPG test.zip 12KB

RPG試作プログラム(JavaScript)の一式をまとめたZIPファイルです。 ダウンロード


「マラカト・アルバトリ」とは、アラビア語で「勇者の戦い」です。Google翻訳で調べました。

また、もともとは「ドラゴンクエスト1」のコピーを作っていて、それを公開するのは著作権上まずいので、いろいろアレンジしてアラビアの話になった次第です。


(訪問者のどんなニーズと この記事がつながるか)


2019/7/15(月) 4連休 3日目

筑波山へ行ってきました -[dekake]

日本百名山のひとつで、「男体山」と「女体山」の2つのが特徴。

当日はくもり空で、どちらの頂もスッポリ暗い雲がかかっていました。

さすがに、百名山に登録されているだけあって、ふもとから見ると、ほかの山とは違う「威厳」が確かにありました。


13:16 撮影

13:17 撮影

13:17 撮影

外国人もたくさんいて、日本人の数と比べると、外国人4、日本人6といった具合。いや、3:7かなぁ…

肝心の山の写真がないので、私のイラストを…

▼左の頂が男体山、右が 女体山。

雲の形に悩みましたが、そういうふうにわからないときは、いつか RPG のマップ作りのときに説明させてもらった、「何かの形に見える」のを逆に利用する方法で形作りました。

大したことないんですが、、まぁ、右の雲は「クジラ」を形作り、左の雲はわかりますかね?「カエル」です。

筑波山に登ると、「ガマ」のなんとか という油とかお菓子とか置物など、たくさん売られています。それに ちなんで「カエル」ですね。


(訪問者のどんなニーズと この記事がつながるか)


2019/7/13(土) 4連休 1日目

IchigoCake -[den]

「IchigoCake」という電子回路基板一式を買うため、秋葉原に買い物に行ってきました。

▼IchigoCake という基板

IchigoCake 本体には、テレビ出力と、JavaScript、BASIC などのプログラミング機能があり、作ったプログラムはファミコンみたいにカートリッジ式にできます。

キーボードをつなげてテレビ画面を見ながらプログラミングができるので、ファミコン+BASIC の「ファミリーベーシック」みたいなものだと思えば近いでしょう。

子供向けのプログラミング教材という位置づけの製品であり、私としては自分のゲーム開発用に…という思いもちょっとあります。

最近 流行りの新し物でもあります。


特別これがほしかったというわけではなく、以上のあいまいな魅力(ふちどりワード部)にひっぱられ一式買いそろえることにしました。

「それを買うから秋葉原に行く」のではなく、「秋葉原に行くから理由が必要で、それらを買うことにする」のかもしれません。


IchigoCake をよく知るためのリンク

7/13(土) の秋葉原での足取り

14:55
茨城県の北のほうから特急に乗り、秋葉原駅に降りました。
当日は曇り空でした。

15:00
5分後、ちょっと寄り道で、私が20才のころ働いていたお店の建物の前に来ました。

当時は JC-WORLD というノートパソコンアップグレードのお店でしたが、トレーディングカードのお店に変わっていました。

店内に入ると、なんとなく見覚えが。
「地下がある」と思って階段を下り、その途中で見上げると、当時の彼女さんが階段の上から私を見下ろして笑っていた思い出が浮かんできてしまいました。今の私からは彼女さんなんてありえないので…まぁ昔の話ですわ。
当時地下は薄暗く何かが出そうな物置でしたが、明るいトレカバトルの会場になっていました。

その建物の向かいは焼き鳥の飲み屋さん。20年以上になるのに変わっていない。
このお店の鶏肉の照り焼きのお弁当がおいしかったのを覚えています。

そんな寄り道はさておき…




15:05
今回の目的の「秋月電子」のお店。
「IchigoCake」というシングルマイコンボードとその周辺部品を買いました。

「IchigoCake 組み立てキット」 ¥2800

ファミコンに例えると、ファミコン本体。…がバラバラになっている状態。
基板上に高性能なマイコンが2つ載っています。

その片方は IchigoJam というすでにある製品のマイコンで、もう片方が PanCake というこれもすでにある製品のマイコンです。2つ合わせて、IchigoCakeってわけか…。
周辺部品「IchigoJam 用外部記憶装置キット IchigoROM」 ¥280

ファミコンに例えると、ファミコンのカセット。…がバラバラになっている状態。
基板上に EEPROM という ROM が載っています。
容量は 1MB なので、大昔の言い方で「メガロム」だな。
周辺部品「TFT LCD MONITOR」 ¥2700

ファミコンに例えると、ファミコンをつなげるテレビ。…がとても小さい状態。
IchigoCake はテレビ出力を2つ持っていて、片方をゲーム画面、片方をプログラミング画面として使えるらしいです。
「マルチモニタ環境」と言えます。

秋月 店内のどこに置いてあるのかわからず、店内に置いてあったチラシを取って目的のモニタの写真を探して、店員さんに「コレほしいんですが」と言ったら、棚の上に並べてあるのを取ってくれました。
 


15:25
秋月のおとなりの「千石電商」。

コンポジット ビデオケーブル
IchigoCake をテレビにつなげるためのビデオケーブルを買いました。

16:30
IchigoCake が PS/2 タイプのキーボードしか受け付けないとのことで、1時間くらい探しました。
なんで USB タイプじゃないんだ?
ツクモ、オノデン、ソフマップなどを渡り歩いて、「ここになかったらあきらめよう」と思って最後に入ったビックカメラで、1種類だけありました。コジマップ、ビックマップ、あれ?そふまっぷと打って「祖父マップ」と変換しなくなったぞ。(それでいいんだよ)

「BUFFALO G:300 ゲーミングキーボード」¥1550

ゲーミングである必要はないんですが、IchigoCake で作るものがゲームなので、ある意味おあつらえ向き かもしれません。
2017年2月 発売なのでそんなに古いものでもないです。

16:50
2時間ものあいだ歩き続け、だいぶ疲れました。
4連休の初日だし、外国人もいっぱいだし。


買い忘れた

後から気づきましたが買い忘れがありました。

購入した IchigoCake は「組み立てキット」なのであって、基板上の IC にはプログラムが何も入っていない(未プログラム)状態で、自分で書き込みしなくてはなりません。

IC に書き込むためには、

パソコンUSB端子 → USB-シリアル変換 → IchigoCake

という接続をしなくてはならないようで、この「USBシリアル変換」というパーツを購入していませんでした。


買い忘れた、そして、12Vもかけた

でも、自宅に「USBシリアル変換ケーブル」があるのでそのケーブルの先のシリアルコネクタからピンを引っ張り出して IchigoCake に接続すればなんとかなるかなと思いました。シリアルで通信すればいいんですから…?


▼ USBシリアル変換ケーブルの先からピンを引っ張り出し…
▼IchigoCake に接続…(してはいけません)


しかし、書き込みできず夜通し奮闘していました。

作業の最後に、シリアルはシリアルでも「USBシリアル変換ケーブル」は RS-232C というシリアルに変換するものであることに気づきました。

あるサイト様(ここ)によると、電子工作の基板のシリアルとRS232Cをつなぐには、そのままではダメで、電圧レベルを変換するとか…

ナニイ!と思って、急いでオシロスコープを使ってコネクタの信号をつかまえて波形の上下幅を見ると 12V もR(あ~る)ではありませんか!

あ…、と思って

わらにもすがる思いで、IchigoCake のマイコンのデータシートも急いで調べると、信号の上限は 5V?

だめじゃないですか、12V もかけちゃ。


いさぎよく、秋月通販にアクセスして、「【M-08461】 FT234X 超小型USBシリアル変換モジュール」¥600を購入しました。(これなら 3.3V です)

届いたら、こちらのサイト様の説明を元に作業しようと思います。

USB が届いても、ボードは壊れているかもしれないので、そのときは買いなおしになるので、しばらく おあずけですね。


(訪問者のどんなニーズと この記事がつながるか)


2019/7/11(木)

iTunesで購入した曲

『ああ、卒業式で泣かないと~♪』の曲を聴きたくなりました。

しかしそのフレーズしかわからないので、フレーズでWEB検索して題名をつきとめました。「卒業」ですね。

斉藤由貴が歌っているとは30年ものあいだ知りませんでした。結構魅力のある人だったんですね。

斉藤由貴の「卒業」もいろいろな収録があるんですが、そのなかでも「AXIA (リマスター盤)」というアルバムの「卒業」がいちばんグッとくる歌い方をしていると思い、それを購入しました。

iTunes の営業手法("リスナーはこんな商品も購入しています")に乗ってしまい、他にも2曲購入してしまいました。


(訪問者のどんなニーズと この記事がつながるか)


2019/7/6(土)

背景の月が大きいあの風景を -[modeling]

スマートホンなどで月を撮影すると、下図のように思ったよりも小さい月になってしまいます。

(レンズサイズ75mm)

下図のように月を大きく撮影するにはどうしたらいいのでしょうか。

(レンズサイズ2000mm)

レンズを替えて、遠く離れて撮影すれば月が大きくなります。

具体的には下図のように、レンズを 2000mm に取り換えて、22m 離れて撮影します。

私は撮影の専門家ではなく、今回 Shade3D という 3DCG ソフトでそういう設定でレンダリングして確認したことを根拠にしてそのように言っています。


この実験をするために、Wikipediaでそれぞれの大きさや距離を調べて、実際の値を入力して3Dの景観を作成しました。

地球
(下部の青い部分)
形状: 球
半径: 6,356 km
位置(x,y,z): 0km, -6,356km, 0km
形状: 球
半径: 1,737 km
位置(x,y,z): -500km, 1,601km, -384,400km
被写体 形状: ポリゴン
サイズ: 20cm x 20cm x 20cm (だいたい)
位置(x,y,z): 0, 0, 0

Shade3D では km 単位のモデル作成もできます。

ただ、被写体(人間)のサイズと、地球などの星のサイズの差が極端に大きすぎるせいか、人と星をShade3Dの画面に同居させると、下図のように画面が不正になってしまいます。

そこで、背景(地球と月)と、被写体を別ファイルに分けて、カメラの設定は同じにして、べつべつにレンダリングして合成しました。





また、カメラのちょっとした向きのずれで被写体や月が大きく動いてしまったり、極端な大小の差のせいか数値入力もうまくいかないので、この3DCGを作るのは一苦労です。

もちろん、3DCGで風景を作るときは、わざわざ現実のサイズでモデリングしなくても、同じような風景になるように小さい月を空に浮かべれば済むことです。


大きな月の撮影は 2000mm のレンズに交換すれば撮影できるということですが、具体的には「望遠レンズ」や「望遠カメラ」を用意することになります。

望遠カメラ(Nikon COOLPIX P-900)※月撮影モードがある


(訪問者のどんなニーズと この記事がつながるか)




webappsrcの確認

1. %%com.webapp.src:/webappsrccheck.html%%
と記述した場合

webapps/src/default.cssのスタイル指定が効く
<!DOCTYPE html><!--ESCAPEPROCESS-->

<head>

<script>

function onloadx() {

//一般関数

console.log( "文字列" );

}

function Class1() {

//クラス

console.log( "文字列" );

}

Class1.prototype.method1 = function() {

//メソッド

console.log( "文字列" );

}

</script>

</head>

<body onload="onloadx();" style="">

Hello world!<BR>

</body>

</html>



2. <code>
%%com.webapp.src:/webappsrccheck.html%%
</code>
と記述した場合

このファイルのcodeのスタイル指定が効く
<!DOCTYPE html><!--ESCAPEPROCESS-->

<head>

<script>

function onloadx() {

//一般関数

console.log( "文字列" );

}

function Class1() {

//クラス

console.log( "文字列" );

}

Class1.prototype.method1 = function() {

//メソッド

console.log( "文字列" );

}

</script>

</head>

<body onload="onloadx();" style="">

Hello world!<BR>

</body>

</html>



3.
%%com.webapp.src:/webappsrccheck2.html,/webappsrccheck.html%%
と記述した場合

webapps/src/default.cssのスタイル指定が効く
<!DOCTYPE html><!--ESCAPEPROCESS-->

<head>

<script>

function onloadx() {

//一般関数 コメント変更

console.log( "文字列変更" );

行追加

}

function Class1() {

//クラス コメント変更

console.log( "文字列変更" );

行追加

}

Class1.prototype.method1 = function() {

//メソッド コメント変更

console.log( "文字列変更" );

行追加

}

</script>

</head>

<body onload="onloadx();文字列変更" style="">

Hello world!<BR>

HTML追加

</body>

</html>



4. <code>
%%com.webapp.src:/webappsrccheck2.html,/webappsrccheck.html%%</code>
と記述した場合

このファイルのcodeのスタイル指定が効く
<!DOCTYPE html><!--ESCAPEPROCESS-->

<head>

<script>

function onloadx() {

//一般関数 コメント変更

console.log( "文字列変更" );

行追加

}

function Class1() {

//クラス コメント変更

console.log( "文字列変更" );

行追加

}

Class1.prototype.method1 = function() {

//メソッド コメント変更

console.log( "文字列変更" );

行追加

}

</script>

</head>

<body onload="onloadx();文字列変更" style="">

Hello world!<BR>

HTML追加

</body>

</html>



5. リンクで
src?webappsrccheck.html
と記述した場合

webapps/src/default.cssのスタイル指定が効く
開く

6. リンクで
src?webappsrccheck2.html,webappsrccheck.html
と記述した場合

webapps/src/default.cssのスタイル指定が効く
開く